package com.mlog.njyjy.ndop.share.sql.support;

import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.mlog.njyjy.ndop.share.sql.annotation.Column;
import com.mlog.njyjy.ndop.share.sql.annotation.Id;
import com.mlog.njyjy.ndop.share.sql.annotation.Table;
import org.springframework.lang.NonNull;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Date;
import java.util.Optional;

/**
 * <p>
 * .
 * </p>
 *
 * @author <a href="mailto:yingxiufeng@mlogcn.com">alex</a>
 * @version v1.0, 2020/7/8
 */
public final class SqlTableUtil {

    private static final String PK_DEFAULT_FIELD = "id";

    private static final Class<?>[] PRIMITIVE_TYPES = {int.class, long.class, short.class,
            float.class, double.class, byte.class, boolean.class, char.class, Integer.class, Long.class,
            Short.class, Float.class, Double.class, Byte.class, Boolean.class, Character.class};

    /**
     * get table name by model class
     *
     * @param clazz
     * @return
     */
    public static String getTblName(@NonNull Class<?> clazz) {
        Table annotation = clazz.getDeclaredAnnotation(Table.class);
        String prefix = "";
        String name = "";
        if (annotation != null) {
            prefix = StrUtil.trim(annotation.prefix());
            name = StrUtil.trim(annotation.name());
        }
        return prefix.concat(StrUtil.isNotBlank(name) ? name : StrUtil.toUnderlineCase(clazz.getSimpleName()).toLowerCase());
    }

    /**
     * 从model 类中找到标记的主键列
     * 如果没有标记则默认为 `id`
     *
     * @param tblClass
     * @return
     */
    public static SqlTableColumn findPkColumn(Class<?> tblClass) {
        Field[] fields = ReflectUtil.getFieldsDirectly(tblClass, false);
        Optional<Field> pkField = Arrays.stream(fields).filter(field -> {
            if (SqlTableUtil.PK_DEFAULT_FIELD.equals(field.getName())) {
                return true;
            }
            Id id = field.getAnnotation(Id.class);
            return id != null;
        }).findFirst();

        if (pkField.isPresent()) {
            Field field = pkField.get();
            Class<?> type = field.getType();
            Id column = field.getAnnotation(Id.class);
            SqlTableColumn idColumn = new SqlTableColumn()
                    .setField(StrUtil.toUnderlineCase(field.getName()))
                    .setNullable(false)
                    .setPk(true);
            if (int.class.isAssignableFrom(type) || Integer.class.isAssignableFrom(type)) {
                idColumn.setExtra("AUTO_INCREMENT").setSqlType(StrUtil.format("int({})", (column != null && column.length() > 0) ? column.length() : 11));
            } else if (long.class.isAssignableFrom(type) || Long.class.isAssignableFrom(type)) {
                idColumn.setExtra("AUTO_INCREMENT").setSqlType(StrUtil.format("bigint({})",
                        (column != null && column.length() > 0) ? column.length() : 20));
            } else if (String.class.equals(type)) {
                idColumn.setSqlType(StrUtil.format("varchar({})", (column != null && column.length() > 0) ? column.length() : 32));
            } else {
                throw new SqlOrmException(StrUtil.format("primary key [{}] should be `int` or `long` or `string`", field.getName()));
            }
            return idColumn;
        }
        return null;
    }

    /**
     * 数据表列 子句
     *
     * @param column
     * @return
     */
    public static String columnClause(SqlTableColumn column) {
        //  `applicant` varchar(256) NOT NULL DEFAULT '' COMMENT '申请人'
        // `id` bigint(128) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
        String tpl = "`{}` {} {} {} {} COMMENT '{}'";
        return StrUtil.format(tpl,
                column.getField(), column.getSqlType(),
                column.isNullable() ? "NULL" : "NOT NULL",
                StrUtil.isNotBlank(column.getExtra()) ? column.getExtra() : "",
                (column.getDefaultValue() != null && !column.isPk()) ? StrUtil.format("DEFAULT '{}'", String.valueOf(column.getDefaultValue())) : "",
                column.getColumnAnnotation() != null ? column.getColumnAnnotation().comment() : StrUtil.replace(column.getField(), "_", " "));
    }

    /**
     * model field to db column
     *
     * @param field
     * @return
     */
    public static SqlTableColumn field2Column(@NonNull Field field, @NonNull Class<?> tblCls) {
        String sqlType = sqlType(field);
        Column annotation = field.getAnnotation(Column.class);
        SqlTableColumn tableColumn = new SqlTableColumn()
                .setSqlType(sqlType)
                .setUnique(annotation != null && annotation.unique())
                .setField((annotation != null && !annotation.underline()) ? field.getName() : StrUtil.toUnderlineCase(field.getName()))
                .setNullable(annotation != null ? annotation.nullable() : true)
                .setColumnAnnotation(annotation);
        // 默认值
        Object fieldValue = ReflectUtil.getFieldValue(ReflectUtil.newInstanceIfPossible(tblCls), field);
        if (fieldValue != null) {
            tableColumn.setDefaultValue(fieldValue);
        }
        return tableColumn;
    }

    /**
     * TODO: enum?
     * model field to sql type
     *
     * @param field field of pojo
     * @return int(11), varchar(32) tinyint(1)
     */
    private static String sqlType(@NonNull Field field) {
        String sqlType = "varchar";
        Column column = field.getAnnotation(Column.class);
        if (column != null && StrUtil.isNotBlank(column.definition())) {
            //  优先使用注解定义类型
            return column.definition();
        }
        Class<?> type = field.getType();
        if (isPrimitiveOrString(type)) {
            if (String.class.equals(type)) {
                sqlType = "varchar(128)";
                if (column != null && column.length() > 0) {
                    sqlType = StrUtil.format("varchar({})", column.length());
                }
            } else if (long.class.isAssignableFrom(type) || Long.class.isAssignableFrom(type)) {
                sqlType = "bigint(20)";
                if (column != null && column.length() > 0) {
                    sqlType = StrUtil.format("bigint({})", column.length());
                }
            } else if (int.class.isAssignableFrom(type) || Integer.class.isAssignableFrom(type)) {
                sqlType = "int(11)";
                if (column != null && column.length() > 0) {
                    sqlType = StrUtil.format("int({})", column.length());
                }
            } else if (float.class.isAssignableFrom(type) || Float.class.isAssignableFrom(type)) {
                sqlType = "float";
                if (column != null && column.precision() > 0) {
                    sqlType = StrUtil.format("float({},{})", column.precision(),
                            column.scale() > 0 ? column.scale() : 2);
                }
            } else if (double.class.isAssignableFrom(type) || Double.class.isAssignableFrom(type)) {
                sqlType = "double";
                if (column != null && column.precision() > 0) {
                    sqlType = StrUtil.format("double({},{})", column.precision(),
                            column.scale() > 0 ? column.scale() : 4);
                }
            } else if (boolean.class.isAssignableFrom(type) || Boolean.class.isAssignableFrom(type)) {
                sqlType = "tinyint(1)";
            }

        } else {
            if (Date.class.equals(type) || LocalDateTime.class.equals(type)) {
                sqlType = "datetime";
            } else if (LocalDate.class.equals(type)) {
                sqlType = "date";
            } else if (BigDecimal.class.equals(type)) {
                sqlType = "decimal";
                if (column != null && column.precision() > 0) {
                    sqlType = StrUtil.format("decimal({},{})", column.precision(),
                            column.scale() > 0 ? column.scale() : 4);
                }
            }
        }
        return sqlType;
    }

    /**
     * is primitive or not
     *
     * @param type
     * @return
     */
    private static boolean isPrimitiveOrString(Class<?> type) {
        if (String.class.equals(type)) {
            return true;
        }
        for (Class<?> standardPrimitive : PRIMITIVE_TYPES) {
            if (standardPrimitive.isAssignableFrom(type)) {
                return true;
            }
        }
        return false;
    }
}
